﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects.DataClasses;
using Ovvio.Data;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Linq.Expressions;

namespace Ovvio.Business
{
    /// <summary>
    /// Provides base CRUD operations
    /// </summary>
    /// <typeparam name="TKey">Data type of key</typeparam>
    /// <typeparam name="TEntity">The entity object</typeparam>
    /// <typeparam name="TObjectContext">The object context</typeparam>
    public class ServiceBase<TKey, TEntity, TObjectContext> where TEntity : EntityObject, new() where TObjectContext : ObjectContext, new()
    {
        private string _entitySet;
        private string[] _paths;
        
        Expression<Func<TEntity, TKey>> _predicate;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="predicate">
        /// Predicate for filter
        /// example: p => p.Id (where p is TEntity)
        /// </param>
        public ServiceBase(Expression<Func<TEntity, TKey>> predicate)
            : this(string.Empty, string.Empty, predicate)
        {
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="entitySet">
        /// The entity set name.
        /// If blank, default set name will be used.
        /// </param>
        /// <param name="predicate">
        /// Predicate for filter
        /// example: p => p.Id (where p is TEntity)
        /// </param>
        public ServiceBase(string entitySet, Expression<Func<TEntity, TKey>> predicate) : this(entitySet,string.Empty,predicate)
        { 
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="entitySet">
        /// The entity set name.
        /// If blank, default set name will be used.
        /// </param>
        /// <param name="paths">
        /// "|" separated paths for lazy loading of related 
        /// objects
        /// </param>
        /// <param name="predicate">
        /// Predicate for filter
        /// example: p => p.Id (where p is TEntity)
        /// </param>
        public ServiceBase(string entitySet, string paths, Expression<Func<TEntity, TKey>> predicate)
        {
            if (string.IsNullOrEmpty(entitySet))
                _entitySet = EntityDefaults.GetEntitySet<TEntity>();
            else
                _entitySet = entitySet;
            _predicate = predicate;
            if (!string.IsNullOrEmpty(paths))
                _paths = paths.Split('|');
        }

        /// <summary>
        /// Creates a new entity object
        /// Default values are supplied
        /// </summary>
        /// <returns>The entity object</returns>
        public TEntity Create()
        {
            var newEntity = new TEntity();
            OnEntityCreate(newEntity);
            return newEntity;
        }

        /// <summary>
        /// Overridable function for use when providing
        /// default values for a new entity.
        /// </summary>
        /// <param name="entity">
        /// The entity to be set with defaults.
        /// </param>
        protected virtual void OnEntityCreate(TEntity entity)
        {}

        //TODO: Overload for paging
        /// <summary>
        /// Returns a list of entities
        /// </summary>
        /// <returns></returns>
        public IList<TEntity> GetEntities(Func<TEntity, bool> filterPredicate)
        {
            using (var ctx = new TObjectContext())
            {
                var query = CreateQueryWithPath(ctx).Where(filterPredicate);
                if (query.Any())
                    return query.ToList();
            }
            return new List<TEntity>();
        }


        public IList<TEntity> GetEntities()
        {
            using (var ctx = new TObjectContext())
            {
                var query = CreateQueryWithPath(ctx);
                return query.ToList();
            }
        }

        protected ObjectQuery<TEntity> CreateQuery(TObjectContext context)
        {
            return context.CreateQuery<TEntity>("[" + _entitySet + "]");
        }
        

        /// <summary>
        /// Creates a query for Linq processing
        /// </summary>
        /// <param name="context">
        /// The object context
        /// </param>
        /// <returns>
        /// Object query
        /// </returns>
        protected ObjectQuery<TEntity> CreateQueryWithPath(TObjectContext context )
        {
            var query = CreateQuery(context);
            if (_paths != null)
            {
                foreach (var path in _paths)
                    query = query.Include(path);
            }
            return query;
        }

        /// <summary>
        /// Returns an entity through key
        /// </summary>
        /// <param name="key">
        /// The key value filter
        /// </param>
        /// <returns>
        /// The entity object
        /// </returns>
        public TEntity GetEntityByKey(TKey key)
        {
            using (var ctx = new TObjectContext())
            {

                var query = CreateQueryWithPath(ctx).Where<TEntity>(BuildExpression(_predicate, new List<TKey> { key }));
                if (query.Any())
                {
                    var entity = query.First();
                    return entity;
                }
            }
            return null;
        }


        /// <summary>
        /// Persists an entity to database.
        /// Purposely detects new entity through key.
        /// Overwrites all existing keys.
        /// This is last save overwrites.
        /// </summary>
        /// <param name="entity">
        /// Disconnected entity.
        /// </param>
        /// <param name="key">
        /// Entity key to update or add.
        /// </param>
        /// <returns>
        /// Indicates success of saving.
        /// </returns>
        public bool Save(TEntity entity, TKey key)
        {
            if (!OnValidateEntity(entity))
                return false;

            using (var ctx = new TObjectContext())
            {
                    object original;
                    var entityKey = ctx.CreateEntityKey(_entitySet, entity);
                    OnEntityBeforeSave(entity);
                    if (ctx.TryGetObjectByKey(entityKey, out original))
                    {
                        
                        ctx.ApplyPropertyChanges(entityKey.EntitySetName, entity);
                        foreach (var entityrelationship in ((IEntityWithRelationships)original).RelationshipManager.GetAllRelatedEnds())
                        {
                            var oldRef = entityrelationship as EntityReference;

                            if (oldRef != null)
                            {
                                var newRef = ((IEntityWithRelationships)entity).RelationshipManager.GetRelatedEnd(oldRef.RelationshipName, oldRef.TargetRoleName) as EntityReference;
                                oldRef.EntityKey = newRef.EntityKey;
                            }
                        }
                    }

                    else
                        ctx.AddObject(_entitySet, entity);

                

                return ctx.SaveChanges() > 0;
            }
        }

        protected virtual void OnEntityBeforeSave(TEntity entity) {}
        protected virtual bool OnValidateEntity(TEntity entity) { return true; }

        public bool Delete(TEntity entity, TKey key)
        {
            using (var ctx = new TObjectContext())
            {
                var query = ctx.CreateQuery<TEntity>("[" + _entitySet + "]").Where<TEntity>(BuildExpression(_predicate, new List<TKey> { key }));
                 if (query.Any())
                 {
                     var entityForDelete = query.First();
                     ctx.DeleteObject(entityForDelete);
                     return ctx.SaveChanges() > 0;
                 }
            }
            return false;
        }

        public bool Exists(Func<TEntity, bool> filterPredicate)
        {
            using (var ctx = new TObjectContext())
            {
                var query = ctx.CreateQuery<TEntity>("[" + _entitySet + "]").Where(filterPredicate);
                return query.Any();
            }
        }

        public TEntity GetEntity(Func<TEntity, bool> filterPredicate)
        {
            using (var ctx = new TObjectContext())
            {
                var query = CreateQueryWithPath(ctx).Where(filterPredicate);
                if (query.Any())
                    return query.First();
            }
            return null;
        }

        private static Expression<Func<TEntity, bool>> BuildExpression( Expression<Func<TEntity, TKey>> valueSelector, IEnumerable<TKey> values)
        {
            if (null == valueSelector)
            {
                throw new
                    ArgumentNullException("valueSelector");
            }
            if (null == values) { throw new ArgumentNullException("values"); }

            ParameterExpression p = valueSelector.Parameters.Single();
            if (!values.Any())
            {
                return e => false;
            }
            
            var equals = values.Select(value =>
            (Expression)Expression.Equal(valueSelector.Body, Expression.Constant(value,
            typeof(TKey))));
            var body = equals.Aggregate<Expression>((accumulate, equal) =>
            Expression.Or(accumulate, equal));
            return Expression.Lambda<Func<TEntity, bool>>(body, p);
        }
    }
}
